iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
Modern Web

Three.js × WebGL 實戰:打造 HD-2D 像素地下城與即時光影系統系列 第 28 篇

Day 28:打碎數位生硬感!後處理管線(Post-Processing)與鏡頭光學模擬:移軸景深、暗角與輝光解密

  • 分享至 

  • xImage
  •  

在 Day 27 中,我們利用三層冷暖光源模型、貼地高斯接觸陰影以及著色器光學乘積,成功將一張張單薄的 2D 像素紙片人與地磚,雕刻出了厚實的三維體積感。

然而,當你在螢幕前檢視目前的成果時,可能會隱約感到一種「數位人工感」:

  • 整個場景從相機眼前 0.1 公尺到遠方 50 公尺外的城垛,每一個像素都處於絕對清晰的狀態;

  • 畫面四周邊角與中央毫無明暗差異,像是一張完全均勻採樣的電腦繪圖;

  • 火把與壁燈雖然顏色明亮,但光芒死死鎖在像素邊界內,缺乏穿透空氣的溫度。

這種冷硬感,源自於電腦繪圖的數學本質——WebGL 的虛擬相機是一個「理想的針孔相機(Pinhole Camera)」。它沒有實體透鏡、沒有鏡片弧度、也沒有空氣介質。

今天我們將解析 HD-2D 的最後一道視覺魔法——後處理管線(Post-Processing Pipeline)。我們將藉由自研的 TiltShiftVignetteShader 與極限門檻的 UnrealBloomPass,在 WebGL 畫布中模擬真實相機鏡頭的物理瑕疵,徹底將冰冷的 3D 渲染轉化為精緻的「袖珍微縮模型盒(Diorama)」!

一、針孔相機的破綻:為什麼真實感來自「光學缺陷」?

在現實世界中,無論多昂貴的相機鏡頭,都受限於光學玻璃的物理極限:

  • 有限景深(Depth of Field):大光圈鏡頭在對焦於某一距離時,前景與背景必然失焦模糊。

  • 邊角失光(Vignetting / 暗角):光線斜向穿過圓柱形鏡筒時,邊緣通光量衰減,畫面四角自然變暗。

  • 透鏡光暈(Lens Flare & Bloom):強光穿過多枚鏡片時產生微弱內部漫射,形成柔和的光暈。

在傳統 3D 遊戲中,開發者極力消除這些瑕疵;但 HD-2D 為了營造「放在桌上的精緻玩具屋」氛圍,反而需要「主動將光學缺陷請回畫布中」。

在 Day 18 中,我們引入了 EffectComposer,它讓 GPU 在完成 3D 場景繪製後,將整張畫面視為一張全螢幕貼圖(Full-Screen Quad),交由自訂著色器進行逐像素的光學重構。

二、移軸微縮景深(Tilt-Shift):打造玩具屋的散景數學

如果你看過真實世界中的微縮模型攝影,會發現攝影師常使用特殊的「移軸鏡頭(Tilt-Shift Lens)」:透過傾斜鏡頭光軸,使焦點平面極度狹窄,只有中央一條細長切面清晰,上下背景迅速化為奶油般的散景。

大腦接收到這種「極淺景深」的視覺訊號時,會自動推論:「被拍攝的物體尺寸非常微小,相機距離它極近!」

為什麼不使用常規的 3D 深度景深(Depth of Field)?

常規的 3D 景深需要讀取深度緩衝區(Depth Buffer),根據物體的絕對 Z 軸距離計算模糊圓(CoC)。
但在俯視 28 度的 HD-2D 視角中:

  • 如果地面沿著 Z 軸深度模糊,畫面上方的遠處石磚會失焦,但畫面下方的近處地面卻是清晰的;

  • 這種縱向衰減無法形成微縮攝影經典的「水平對稱焦點帶」,而且全景深採樣需要多次雙向高斯模糊,效能開銷極其沉重。

1D 垂直高斯卷積:自研 TiltShift 著色器核心

在專案的自研著色器中,我們採用了一種優雅且兼顧效能的做法——以螢幕垂直 Y 座標為基準的單向垂直高斯卷積。

// TiltShiftVignetteShader.js 片段著色器核心
uniform float focusPos;    // 垂直聚焦中心 (0.46)
uniform float focusRange;  // 清晰焦點帶半徑 (0.20)
uniform float blurAmount;  // 最大模糊半徑 (0.0055)

// 1. 計算當前像素距離水平焦點切線的垂直距離
float dist = abs(vUv.y - focusPos);

// 2. 利用 smoothstep 構建平滑過渡帶
// 距離在 focusRange 內:完全清晰 (blurFactor = 0.0)
// 超出 focusRange:平滑過渡至全模糊 (blurFactor = 1.0)
float blurFactor = smoothstep(focusRange, focusRange + 0.35, dist);
float vv = v * blurAmount * blurFactor;

// 3. 9 採樣高斯加權卷積 (垂直方向)
vec4 sum = vec4(0.0);
sum += texture2D(tDiffuse, vec2(vUv.x, vUv.y - 4.0 * vv)) * 0.0510;
sum += texture2D(tDiffuse, vec2(vUv.x, vUv.y - 3.0 * vv)) * 0.0918;
sum += texture2D(tDiffuse, vec2(vUv.x, vUv.y - 2.0 * vv)) * 0.1224;
sum += texture2D(tDiffuse, vec2(vUv.x, vUv.y - 1.0 * vv)) * 0.1531;
sum += texture2D(tDiffuse, vec2(vUv.x, vUv.y           )) * 0.1633; // 中心採樣權重最高
sum += texture2D(tDiffuse, vec2(vUv.x, vUv.y + 1.0 * vv)) * 0.1531;
sum += texture2D(tDiffuse, vec2(vUv.x, vUv.y + 2.0 * vv)) * 0.1224;
sum += texture2D(tDiffuse, vec2(vUv.x, vUv.y + 3.0 * vv)) * 0.0918;
sum += texture2D(tDiffuse, vec2(vUv.x, vUv.y + 4.0 * vv)) * 0.0510;

高斯權重與 smoothstep 的雙重守護

  • 權重歸一化(Sum = 1.0):9 個採樣點的係數總和精確等於 $1.0$($0.0510 \times 2 + 0.0918 \times 2 + \dots + 0.1633 = 1.0$)。這保證了像素在模糊散焦時,光子能量百分之百守恆,絕不產生發暗或過曝。
  • 中央像素防護罩(focusPos = 0.46, focusRange = 0.20):在畫面縱向 $26% \sim 66%$ 的寬廣中央帶中,blurFactor 恆為 0。
    這確保了我們的主角在跑動、揮杖與待機時,身上的 2D 像素邊界永遠處於零模糊、絕對銳利的狀態;只有上方遠處的高聳城堡殘壁與下方掠過的邊緣石磚,才會化為如夢似幻的奶油散景。

三、電影級暗角(Vignette)與單 Pass 融合哲學

在處理完移軸散景後,下一個光學工序是邊角失光(Vignette)。

壓暗邊角的視覺心理學

在沒有暗角的情況下,玩家的視線容易被四周散落的石塊細節吸引。
暗角透過在螢幕邊界施加一層極其微弱的二次方徑向壓暗:

  • 模擬了真實大口徑微距鏡頭邊緣通光量下降的物理現象;

  • 形成了一道自然的「視覺隧道」,將玩家的注意力下意識收斂聚焦在畫面中央的主角與手中的暖金火把上。

算力省一半:單 Pass 融合架構(Single-Pass Merging)

如果將移軸景深與電影暗角分開寫成兩個獨立的 Shader Pass:

  • 通道 1 讀取 Buffer A,模糊運算後寫入 Buffer B;

  • 通道 2 讀取 Buffer B,暗角運算後寫入 Buffer A。
    這意味著 GPU 每一幀都必須對全螢幕數百萬個像素進行兩次完整的離屏紋理讀寫(Memory Bandwidth)。

我們將暗角公式直接融合在 TiltShiftVignetteShader 的末端:

// 暗角計算:直接在移軸卷積的輸出色彩上就地調和
vec2 uv = (vUv - vec2(0.5)) * vec2(offset); // 以螢幕正中心 (0.5, 0.5) 為原點
float vignetteDist = dot(uv, uv);           // 計算距離中心的平方距離 (極致高效,免開根號)
float vignetteFactor = clamp(1.0 - vignetteDist * darkness, 0.0, 1.0);

// 將移軸高斯模糊結果乘上暗角因子
gl_FragColor = vec4(sum.rgb * vignetteFactor, sum.a);
  • dot(uv, uv) 效能優勢:在 GLSL 著色器中,計算長度 length(uv) 需要耗費昂貴的開根號運算(sqrt);直接使用內積 dot(uv, uv) 計算平方距離,不僅省去硬體運算週期,其二次方衰減曲線恰好完美吻合透鏡邊緣失光的物理規律!
  • 單 Pass 效益:在同一次全螢幕像素處理中,同時完成 9 採樣景深模糊與暗角疊加,省下整整 $50%$ 的後處理紋理頻寬,穩守 60 FPS 幀率防線。

四、輝光(Bloom)與空氣介質:0.98 極限門檻的秘密

在純粹的 WebGL 環境中,光線只能照亮物體表面,光斑周圍是沒有「空氣」的。

我們使用 UnrealBloomPass 來模擬強光在鏡片內部與潮濕地窖空氣中產生的微小散射,但如果參數配置不當,像素遊戲會瞬間毀於一旦。

// 後處理輝光配置
const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  0.55,  // strength: 柔和的漫射強度,避免過度泛濫
  0.65,  // radius: 光暈擴散半徑
  0.98   // threshold: 極限亮度門檻!像素防護的核心鎖
);

為什麼是 0.98 的極限門檻?

傳統 3D 遊戲常將 Bloom Threshold 設定在 0.6 ~ 0.8。如果套用在 HD-2D 專案中:

  • 主角的白色衣物、淺色皮膚與地面石磚的高光,亮度通常介於 0.85 ~ 0.95;

  • 一旦門檻設得太低,角色的臉部與手臂會同時向外發光,整個人像是一團過曝的發光果凍,像素特有的邊界稜角瞬間被光暈吞噬。

將門檻死守在 0.98,建立了嚴格的「光學特權階級」:

  1. 常規幾何體(石磚、角色皮膚):受光後最高亮度限制在 0.96,被完全阻擋在輝光名單之外,維持絕對乾淨銳利的像素質感。
  2. 自發光超標體(火把火焰核心、壁燈發光體):自發光強度高達 1.6~2.3,在著色器中輕鬆穿透 0.98 的防線。

只有真正的火光能引發光暈。在冷暗深邃的地宮中,一抹溫暖的暖金色光暈在火把周圍微微蒸騰,空氣的濕度與熱量在此時被直觀地具象化!

【本日結語】
今天我們透過後處理管線,徹底瓦解了 WebGL 針孔相機的冰冷數位感:

  • 移軸景深(Tilt-Shift):利用 1D 垂直 9 採樣高斯卷積,打造中央清晰、上下散焦的微縮模型視錐。

  • 電影暗角(Vignette):以 dot(uv, uv) 零成本徑向衰減,將光學焦點壓回中央主角。

  • 單 Pass 融合:將景深與暗角合併於單一 GLSL 通道,為顯卡節省 50% 頻寬。

  • 0.98 極限輝光門檻:在保護 2D 像素銳利邊界的同時,喚醒火把穿透空氣的溫暖漫射。

至此,整座 HD-2D 像素地宮的視覺渲染體系已全數解密完畢!

然而,這些頂級的畫面在你的高階開發電腦上跑得很順暢,到了使用者的手機、Retina 高解析度筆電或是低階內顯裝置上,是否依然能穩如泰山?

在明天的 Day 29——技術精講完結篇中,我們將探討最後的實戰難題:瀏覽器裡的 3D 渲染哲學:畫質、效能與 Web 限制的極限平衡!我們將深入剖析 Retina 螢幕的高 DPI 像素陷阱(devicePixelRatio)、視窗矩陣自適應,以及 WebGL 上下文遺失(Context Lost)的容錯防禦機制!


上一篇
Day 27:立體感的靈魂!Three.js 即時光學架構如何賦予 2D 平面「三維體積」
下一篇
Day 29:技術精講完結篇!瀏覽器裡的 3D 渲染哲學:畫質、效能與 Web 限制的極限平衡
系列文
Three.js × WebGL 實戰:打造 HD-2D 像素地下城與即時光影系統 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言